/*
 * Created on 2004-10-27
 */
package dk.aiszone.lang.html;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;

import dk.aiszone.lang.autosource.SourceFileGenerator;
import dk.aiszone.lang.autosource.generatorobjects.Class;
import dk.aiszone.lang.autosource.generatorobjects.Import;
import dk.aiszone.lang.autosource.generatorobjects.JavaFile;
import dk.aiszone.lang.autosource.generatorobjects.helpers.ImportsList;
import dk.aiszone.lang.html.autosource.ElementClass;
import dk.aiszone.lang.html.autosource.EnumClass;

/**
 * 
 * 
 * @author Ais
 */
public class HTMLDomainModelGenerator {

    private static final Set<String> excludedObjects = new HashSet<String>();

    static {
        excludedObjects.add("abbr");
        excludedObjects.add("acronym");
        excludedObjects.add("address");
        excludedObjects.add("align");
        excludedObjects.add("annotation-xml"); // MathML
        excludedObjects.add("applet");
        excludedObjects.add("basefont");
        excludedObjects.add("b");
        excludedObjects.add("big");
        excludedObjects.add("blockquote");
        excludedObjects.add("bq");
        excludedObjects.add("caption");
        excludedObjects.add("cite");
        excludedObjects.add("code");
        excludedObjects.add("del");
        excludedObjects.add("dfn");
        excludedObjects.add("dir");
        excludedObjects.add("em");
        excludedObjects.add("fn");
        excludedObjects.add("font");
        excludedObjects.add("h1");
        excludedObjects.add("h2");
        excludedObjects.add("h3");
        excludedObjects.add("h4");
        excludedObjects.add("h5");
        excludedObjects.add("h6");
        excludedObjects.add("i");
        excludedObjects.add("ins");
        excludedObjects.add("isindex");
        excludedObjects.add("kbd");
        excludedObjects.add("menu");
        excludedObjects.add("q");
        excludedObjects.add("root"); // MathML
        excludedObjects.add("s");
        excludedObjects.add("samp");
        excludedObjects.add("small");
        excludedObjects.add("smap");
        excludedObjects.add("strike");
        excludedObjects.add("strong");
        excludedObjects.add("tt");
        excludedObjects.add("u");
        excludedObjects.add("valign");

    }

    public static void main(String[] args) {
        if (args.length == 0 || args.length > 5) {
            System.out.println("Usage: argument 1 = mapping file, argument 2 = destination folder, argument 3 = destination file, argument 4 = package, argument 5 = class prefix");
        } else {
            try {
                CreateHTMLDomainModel model = new CreateHTMLDomainModel(args[0], args[2], args[3]);

                Collections.sort(model.objects, new Comparator<JavaFile>() {

                    @Override
                    public int compare(JavaFile o1, JavaFile o2) {
                        return o1.getMainClass().getName().compareTo(o2.getMainClass().getName());
                    }

                });
                for (JavaFile javaFile : model.objects) {
                    SourceFileGenerator.writeFile(args[1], javaFile);
                }

                System.out.println("Done");
            } catch (Exception e) {
                e.printStackTrace();
                System.exit(-1);
            }
        }
        System.exit(0);
    }

    /**
     * Class containing an attribute.
     */
    public static class Attribute {

        /** The name of the attribute */
        public final String name;

        /** The type of the attribute */
        public final Type type;

        /* pp */Attribute(String name, Type type) {
            if (name == null) {
                throw new IllegalArgumentException("name");
            }
            if (type == null) {
                throw new IllegalArgumentException("type");
            }

            this.name = name;
            this.type = type;
        }

    }

    /**
     * Interface defining a restriction to the value of an attribute.
     */
    public static interface Restriction {

        /**
         * Convert the restriction to Java source.
         * 
         * @param variableName
         *            The name of the variable being to generate the restriction code for.
         * 
         * @return The java source representation of the <code>Restriction</code>.
         */
        public String toJava(String variableName);

        public ImportsList getNeededImports();

    }

    /**
     * The type of an attribute
     */
    public static class Type {

        /** The class name of the type */
        public final String _class;

        /** <b>Optionally</b> the restriction to the type */
        public final Restriction restriction;

        /* pp */Type(String _class, Restriction restriction) {
            this._class = _class;
            this.restriction = restriction;
        }
    }

    /* pp */static class CreateHTMLDomainModel {

        private static final Restriction NON_NEGATIVE_DEFAULT = new IntPatternRestriction("0", null);

        private final String _package;
        private Map<String, List<Attribute>> attributeGroups;
        private Map<String, TypeElement> complexTypes;
        private Map<String, TypeElement> elements;
        private final Namespace ns;
        private Map<String, List<String>> objectGroups;
        private List<JavaFile> objects = new ArrayList<JavaFile>();

        private Map<String, Type> simpeTypes;

        /* pp */CreateHTMLDomainModel(String file, String _package, String classPrefix) throws Exception {
            this._package = _package;

            SAXBuilder builder = new SAXBuilder(false);
            Document document = builder.build(file);
            Element root = document.getRootElement();
            ns = root.getNamespace();

            simpeTypes = retrieveSimpleTypes(root);
            attributeGroups = retrieveAttributeGroups(root);
            objectGroups = retrieveObjectGroups(root);
            complexTypes = retrieveComplexTypes(root);
            elements = retrieveElements(root);

            objects.addAll(createJavaFiles());
        }

        private Attribute createAttribute(Element attribute) {
            String name = attribute.getAttributeValue("name");
            if (name == null) {
                return null;
            }

            String type = attribute.getAttributeValue("type");
            if (type == null) {
                Element simpleType = attribute.getChild("simpleType", ns);
                if (simpleType != null) {
                    Element restriction = simpleType.getChild("restriction", ns);

                    return new Attribute(name, new Type(createEnum(name, restriction), null));
                }
                return new Attribute(name, new Type("java.lang.String", null));
            }
            return new Attribute(name, simpeTypes.get(type));
        }

        @SuppressWarnings("unchecked")
        private String createEnum(String name, Element restriction) {
            EnumClass _enum = new EnumClass(_package, name);

            for (Iterator<Element> iterator = restriction.getChildren("enumeration", ns).iterator(); iterator.hasNext();) {
                _enum.addEnumValue(iterator.next().getAttributeValue("value"));
            }
            objects.add(_enum);

            return _enum.getPackage() + "." + _enum.getMainClass().getName();
        }

        private Collection<? extends JavaFile> createJavaFiles() {
            List<JavaFile> result = new ArrayList<JavaFile>();

            for (Entry<String, TypeElement> entry : elements.entrySet()) {
                ElementClass elementClass = new ElementClass(_package, entry.getKey());
                for (Attribute attribute : entry.getValue().attributes) {
                    elementClass.addAttribute(attribute);
                }

                for (String element : entry.getValue().elements) {
                    elementClass.addElement(new Class(element));
                }

                result.add(elementClass);
            }

            return result;
        }

        private Restriction createRestriction(Element restriction) {
            return createRestriction(restriction, null);
        }

        private Restriction createRestriction(Element restriction, Restriction _default) {
            Element pattern = restriction.getChild("pattern", ns);
            if (pattern != null) {
                return new StringPatternRestriction(pattern.getAttributeValue("value"));
            }

            Element length = restriction.getChild("length", ns);
            if (length != null) {
                return new StringLengthRestriction(length.getAttributeValue("value"));
            }

            Element minInclusive = restriction.getChild("minInclusive", ns);
            Element maxInclusive = restriction.getChild("maxInclusive", ns);
            if (minInclusive != null || maxInclusive != null) {
                return new IntPatternRestriction((minInclusive != null ? minInclusive.getAttributeValue("value") : null), (maxInclusive != null ? maxInclusive.getAttributeValue("value") : null));
            }

            if (!restriction.getChildren().isEmpty()) {
                throw new IllegalArgumentException("Unkown restriction");
            }

            return _default;
        }

        private Element getParent(Element complexType) {
            Element sequence = complexType.getChild("sequence", ns);
            if (sequence != null) {
                return getParent(sequence);
            }
            Element choice = complexType.getChild("choice", ns);
            if (choice != null) {
                return getParent(choice);
            }
            return complexType;
        }

        @SuppressWarnings("unchecked")
        private TypeElement handleComplexType(Element complexType, TypeElement base) {
            TypeElement typeElement = (base != null ? (TypeElement)base.clone() : new TypeElement());
            if (Boolean.valueOf(complexType.getAttributeValue("mixed")).booleanValue()) {
                typeElement.elements.add("java.lang.String");
            }

            Element parent = getParent(complexType);
            for (Iterator<Element> iterator = parent.getChildren().iterator(); iterator.hasNext();) {
                Element child = iterator.next();

                if ("group".equals(child.getName())) {
                    List<String> objects = objectGroups.get(child.getAttributeValue("ref"));
                    typeElement.elements.addAll(objects);
                } else if ("element".equals(child.getName())) {
                    if (excludedObjects.contains(child.getAttributeValue("ref"))) {
                        continue;
                    }
                    typeElement.elements.add("dk.aiszone.lang.html." + capatialize(child.getAttributeValue("ref")));
                } else if ("attribute".equals(child.getName())) {
                    Attribute attribute = createAttribute(child);
                    if (attribute != null) {
                        typeElement.attributes.add(attribute);
                    }
                } else if ("attributeGroup".equals(child.getName())) {
                    typeElement.attributes.addAll(attributeGroups.get(child.getAttributeValue("ref")));
                } else if ("complexContent".equals(child.getName())) {
                    return handleComplexType(child, null);
                } else if ("extension".equals(child.getName())) {
                    return handleComplexType(child, complexTypes.get(child.getAttributeValue("base")).clone());
                } else {
                    throw new RuntimeException("Unknown tag: " + child.getName());
                }
            }
            return typeElement;
        }

        @SuppressWarnings("unchecked")
        private Map<String, List<Attribute>> retrieveAttributeGroups(Element root) {
            Map<String, List<Attribute>> result = new HashMap<String, List<Attribute>>();

            List<Element> attributeGroups = root.getChildren("attributeGroup", ns);
            for (Element attributeGroup : attributeGroups) {
                String groupName = attributeGroup.getAttributeValue("name");

                List<Attribute> attributes = new ArrayList<Attribute>();
                for (Iterator<Element> iterator = attributeGroup.getChildren("attribute", ns).iterator(); iterator.hasNext();) {
                    Attribute attribute = createAttribute(iterator.next());
                    if (attribute != null) {
                        attributes.add(attribute);
                    }
                }

                for (Iterator<Element> iterator = attributeGroup.getChildren("attributeGroup", ns).iterator(); iterator.hasNext();) {
                    Element childAttributeGroup = iterator.next();

                    attributes.addAll(result.get(childAttributeGroup.getAttributeValue("ref")));
                }

                result.put(groupName, attributes);
            }

            return result;
        }

        @SuppressWarnings("unchecked")
        private Map<String, TypeElement> retrieveComplexTypes(Element root) {
            Map<String, TypeElement> result = new HashMap<String, TypeElement>();

            List<Element> complexTypes = root.getChildren("complexType", ns);
            for (Element complexType : complexTypes) {
                String complexTypeName = complexType.getAttributeValue("name");
                TypeElement typeElement = handleComplexType(complexType, null);

                result.put(complexTypeName, typeElement);
            }

            return result;
        }

        @SuppressWarnings("unchecked")
        private Map<String, TypeElement> retrieveElements(Element root) {
            Map<String, TypeElement> result = new HashMap<String, TypeElement>();

            List<Element> elements = root.getChildren("element", ns);
            for (Element element : elements) {
                String elementName = element.getAttributeValue("name");
                if (excludedObjects.contains(elementName)) {
                    continue;
                }

                TypeElement typeElement = handleComplexType(element.getChild("complexType", ns), null);
                result.put(elementName, typeElement);
            }

            return result;
        }

        @SuppressWarnings("unchecked")
        private Map<String, List<String>> retrieveObjectGroups(Element root) {
            Map<String, List<String>> result = new HashMap<String, List<String>>();

            List<Element> groups = root.getChildren("group", ns);
            for (Element group : groups) {
                String groupName = group.getAttributeValue("name");

                Element parent = group;
                Element sequence = parent.getChild("sequence", ns);
                if (sequence != null) {
                    parent = sequence;
                }
                Element choice = parent.getChild("choice", ns);

                List<String> choices = new ArrayList<String>();
                for (Iterator<Element> iterator = choice.getChildren().iterator(); iterator.hasNext();) {
                    Element child = iterator.next();

                    if ("group".equals(child.getName())) {
                        choices.addAll(result.get(child.getAttributeValue("ref")));
                    } else if ("element".equals(child.getName())) {
                        if (excludedObjects.contains(child.getAttributeValue("ref"))) {
                            continue;
                        }
                        choices.add("dk.aiszone.lang.html." + capatialize(child.getAttributeValue("ref")));
                    } else {
                        throw new RuntimeException("Unknown tag: " + child.getName());
                    }
                }
                result.put(groupName, choices);
            }

            return result;
        }

        @SuppressWarnings("unchecked")
        private Map<String, Type> retrieveSimpleTypes(Element root) {
            Map<String, Type> result = new HashMap<String, Type>();

            result.put("xs:ID", new Type("java.lang.String", null));
            result.put("xs:NMTOKENS", new Type("java.lang.String", null));
            result.put("xs:NMTOKEN", new Type("java.lang.String", null));
            result.put("xs:IDREF", new Type("java.lang.String", null));
            result.put("xs:IDREFS", new Type("java.lang.String", null));

            List<Element> simpleTypes = root.getChildren("simpleType", ns);
            for (Element simpleType : simpleTypes) {
                String name = simpleType.getAttributeValue("name");

                Element restriction = simpleType.getChild("restriction", ns);
                String base = restriction.getAttributeValue("base");

                Type type = result.get(base);
                if (type == null) {
                    if (base.indexOf(':') > -1) {
                        base = base.substring(ns.getPrefix().length() + 1);
                    }

                    if ("string".equals(base) || "language".equals(base) || "anyURI".equals(base)) {
                        type = new Type("java.lang.String", createRestriction(restriction));
                    } else if ("token".equals(base)) {
                        type = new Type(createEnum(name, simpleType.getChild("restriction", ns)), null);
                    } else if ("nonNegativeInteger".equals(base) || "Number".equals(base)) {
                        type = new Type("java.lang.String", createRestriction(restriction, NON_NEGATIVE_DEFAULT));
                    } else if ("dateTime".equals(base)) {
                        type = new Type("java.util.Date", null);
                    } else {
                        throw new RuntimeException("Unknown class for " + name + ":" + base);
                    }
                }

                result.put(name, type);
            }

            return result;
        }

        private String capatialize(String name) {
            return name.substring(0, 1).toUpperCase() + name.substring(1);
        }
    }

    private static class IntPatternRestriction implements Restriction {

        private final String max;
        private final String min;

        /* pp */IntPatternRestriction(String min, String max) {
            this.min = min;
            this.max = max;
        }

        @Override
        public String toJava(String variableName) {
            StringBuilder sb = new StringBuilder();
            sb.append("\nif(").append(variableName).append(" != null) {\n");
            sb.append("int _int = Integer.parseInt(").append(variableName).append(");\n");
            sb.append("\nif(");
            if (min != null) {
                sb.append("_int <= ").append(min);
            }
            if (min != null && max != null) {
                sb.append(" || ");
            }
            if (max != null) {
                sb.append("_int >= ").append(max);
            }
            sb.append(") {\n");
            sb.append("throw new IllegalArgumentException(").append(variableName).append(" + \" doesn't match bounds [").append(min).append(" ; ").append((max == null ? "inf" : max)).append("]\");\n");
            sb.append("}\n");
            sb.append("}\n");

            return sb.toString();
        }

        @Override
        public ImportsList getNeededImports() {
            return null;
        }
    }

    private static class StringLengthRestriction implements Restriction {

        private final String length;

        /* pp */StringLengthRestriction(String length) {
            this.length = length;
        }

        @Override
        public String toJava(String variableName) {
            StringBuilder sb = new StringBuilder();
            sb.append("\nif(").append(variableName).append(" != null && ").append(variableName).append(".length() > ").append(length).append(") {\n");
            sb.append("throw new IllegalArgumentException(").append(variableName).append(" + \" longer then [").append(length).append("]\");\n");
            sb.append("}\n");
            return sb.toString();
        }

        @Override
        public ImportsList getNeededImports() {
            return null;
        }
    }

    private static class StringPatternRestriction implements Restriction {

        private final String pattern;

        /* pp */StringPatternRestriction(String pattern) {
            this.pattern = pattern.replaceAll("\\\\", "\\\\\\\\");
        }

        @Override
        public String toJava(String variableName) {
            StringBuilder sb = new StringBuilder();
            sb.append("\nif(Pattern.matches(\"").append(pattern).append("\", ").append(variableName).append(")) {\n");
            sb.append("throw new IllegalArgumentException(").append(variableName).append(" + \" doesn't match pattern [").append(pattern).append("]\");\n");
            sb.append("}\n");
            return sb.toString();
        }

        @Override
        public ImportsList getNeededImports() {
            ImportsList imports = new ImportsList();
            imports.addImport(new Import("java.util.regex.Pattern"));
            return imports;
        }
    }

    private static class TypeElement implements Cloneable {

        /* pp */final List<Attribute> attributes = new ArrayList<Attribute>();
        /* pp */final List<String> elements = new ArrayList<String>();

        @Override
        public TypeElement clone() {
            TypeElement clone = new TypeElement();

            clone.attributes.addAll(this.attributes);
            clone.elements.addAll(this.elements);

            return clone;
        }
    }
}